Skip to content

feat(file): search workspace files by regular expression - #7370

Merged
icecrasher321 merged 7 commits into
stagingfrom
feat/file-search-regex
Sep 1, 2026
Merged

feat(file): search workspace files by regular expression#7370
icecrasher321 merged 7 commits into
stagingfrom
feat/file-search-regex

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

The File block's Search operation read its query as literal text. It now reads it as a line-oriented regular expression by default, with a Match setting on the block to go back to verbatim text.

Why this needed no migration

The segment store already indexes content with gin_trgm_ops. pg_trgm extracts trigrams from a regex source too, not just a LIKE pattern, so ~ / ~* plan as a bitmap index scan exactly like LIKE / ILIKE. Measured on a 200k-row table with that index:

Predicate Plan
content LIKE '%handleRequestTimeout%' (before) Bitmap Index Scan
content ~ 'handle(Request|Response)Timeout' Bitmap Index Scan
content ~* 'HANDLEREQUEST|handleRequest' Bitmap Index Scan
content ~ '\yhandleRequestTimeout\y' Bitmap Index Scan

No new index, no schema change, no backfill.

Shape

One compiled pattern owns every mode-specific decision — how PostgreSQL matches a segment, whether the segment must hold a whole logical line, and where the match sits inside it — so the repository builds one query shape and the preview renderer one preview shape.

regex.ts    parses the supported subset, computes the guaranteed literal run, rewrites \b → \y
pattern.ts  compileFileSearchPattern(query, mode) → { sqlPattern, caseSensitive,
                                                      literalText, wholeLineOnly, findMatchRange }
repository  builds one query from it
text.ts     renders one preview from it

Compilation happens in the application use case, not the route adapter, so any future surface (v2 API, Copilot) gets identical semantics and the same actionable validation error.

Supported syntax

Deliberately the intersection of PostgreSQL ARE and JavaScript RegExp, because the same source drives both the indexed ~ / ~* predicate and the client-side match location a preview centres on. Anything the two engines read differently is rejected by name rather than silently reinterpreted.

Supported: . * + ? {n,m} and lazy forms, [a-z] / [^0-9], \d \w \s \D \W \S, |, (...) / (?:...), ^ $, \b.

Rejected with a message naming the alternative: lookaround, backreferences, named groups, (?i), \p{...}, POSIX classes, PostgreSQL-only escapes.

\b is a backspace in PostgreSQL, not a word boundary, so it is rewritten to \y on the way out — the one dialect gap a user would otherwise hit blind. No multiline: a match lies within one line.

Not dangerous — four independent layers

  1. Literal-run gate. A pattern must contain 3 consecutive literal characters every match will include. pg_trgm can index nothing shorter, and an unextractable pattern plans as a Parallel Seq Scan across every workspace's segments. error \d+ passes; \d{4}-\d{2}-\d{2} is rejected with a message that says why.
  2. new RegExp proves it compiles in JavaScript.
  3. PostgreSQL proves it compiles in ARE — 2201B becomes a 400, never a 500.
  4. statement_timeout + lock_timeout bound the read, mirroring lib/table/planner.ts. This layer covers exact matching too, closing a pre-existing hole: a punctuation-only or non-ASCII literal query (-->, CJK) has always produced zero trigrams and reached the same cross-tenant scan.

The agent surface

mode is a builder setting, withheld from the model like maxResults — the agent writes the query, the block decides how it is read.

That makes the syntax documentation load-bearing, so toolEnrichment swaps the declared syntax for the active mode's. The two readings disagree on every metacharacter: a regex sent to a block set to exact matching would otherwise be searched for verbatim and silently find nothing. Verified for both modes and for a block saved before the field existed (falls through to the default mode's text).

Verified against a real PostgreSQL 17

The unit tests mock @sim/db, so they never render the SQL the repository emits. A scratch-database harness runs the real thing:

  • 14 behavioural checks end to end — exact mode unchanged, alternation, classes, metacharacter-blind smart case, \b\y, anchors bound to a whole line, split-line previews centred on the match, cross-workspace isolation, guards not leaking past the transaction
  • Live regex search over 150,012 segments in 14ms; 6/6 representative patterns reach the trigram index
  • The guard cutting a deliberately expensive 12s pattern at 10.08s into Search timed out. Narrow the search…

That harness caught a bug the unit tests could not: drizzle wraps a query failure in DrizzleQueryError with no code, and the driver's SQLSTATE sits one level down on cause. The first version read the top level, so the timeout would have surfaced as a generic 500 with no actionable text.

Behaviour change

Regex is the default at every layer, including for a Search block saved before Match existed (no stored moderegex). A query containing regex metacharacters changes meaning or errors: config.json also matches configXjson, handleRequest( fails with "Unclosed (", price: $100 never matches. Fix per block is flipping Match to Exact match.

Note that regex mode is not a strict superset — the trigram gate rejects some plain-text queries exact mode accepts, e.g. 1.2.3 has no 3-character literal run.

Checks

bun run test in apps/sim: 39,472 passed, 0 failures. tsc --noEmit clean, biome clean, check:api-validation passed. All re-run on this branch's base (latest staging) after bun install, since the base moved 5 commits including a deps and a test-sharding change.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 1, 2026 11:30pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR changes workspace file search to support line-oriented regular expressions by default while retaining an exact-match mode.

  • Adds bounded regex parsing, validation, and PostgreSQL-compatible pattern compilation.
  • Moves regex match-location work into the timeout-protected database transaction.
  • Updates block configuration, tool metadata, documentation, mocks, and tests for the new search modes.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported unbounded JavaScript regex execution has been removed and no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/workspace-files/search/pattern.ts Compiles exact and regex modes while ensuring regex preview matching is never executed by JavaScript.
apps/sim/lib/workspace-files/search/regex.ts Parses the supported cross-engine regex subset and enforces depth, repeat, syntax, and literal-run constraints.
apps/sim/lib/workspace-files/search/repository.ts Performs matching and regex match-location work in PostgreSQL under transaction-local statement and lock timeouts.
apps/sim/lib/workspace-files/search/text.ts Builds line previews using database-provided regex ranges or bounded literal matching.
apps/sim/lib/workspace-files/application/search-workspace-file-content.ts Compiles search patterns at the application boundary and maps validation and availability errors appropriately.
apps/sim/blocks/blocks/file.ts Adds the builder-only match-mode setting and defaults existing blocks to regular-expression search.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant UseCase
  participant Compiler
  participant PostgreSQL
  participant Preview
  Caller->>UseCase: Search(query, mode)
  UseCase->>Compiler: compileFileSearchPattern
  Compiler-->>UseCase: Validated SQL pattern
  UseCase->>PostgreSQL: Search and locate match under timeouts
  PostgreSQL-->>UseCase: Segments and match offsets
  UseCase->>Preview: Render matching logical lines
  Preview-->>Caller: Bounded search results
Loading

Reviews (7): Last reviewed commit: "fix(file): close the allowlist around Po..." | Re-trigger Greptile

Comment thread apps/sim/lib/workspace-files/search/pattern.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 17 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Re-trigger cubic

Comment thread apps/sim/lib/workspace-files/search/text.ts Outdated
Comment thread apps/sim/tools/file/search.ts
Comment thread apps/sim/lib/workspace-files/search/repository.ts
Comment thread apps/sim/lib/workspace-files/search/pattern.ts Outdated
Comment thread apps/sim/lib/workspace-files/search/regex.ts
Comment thread apps/sim/lib/workspace-files/search/regex.ts Outdated
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 19 files

Confidence score: 3/5

  • apps/sim/lib/workspace-files/search/repository.ts can discard valid unanchored regex matches on lines over 16,384 characters when an anchored alternative is present, creating a concrete search-result regression; separate whole-line handling per alternative before filtering.
  • apps/sim/lib/workspace-files/search/text.ts can truncate a regex match larger than 2 KiB without an omission marker, so previews may silently misrepresent long matches; reserve space for the marker or explicitly indicate truncation.
  • apps/sim/lib/workspace-files/search/regex.ts has two correctness gaps: misleading replacement advice for \Y, \m, and \M, and rejection of variable repeats whose minimum still establishes the required literal run; correct the suggested alternatives and include minimum repetitions in the derived guarantees.
  • apps/docs/content/docs/integrations/file.mdx inaccurately says uppercase escapes and character classes enable case-sensitive matching, which can mislead users about search behavior; update the documentation to match the implementation.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/docs/content/docs/integrations/file.mdx">

<violation number="1" location="apps/docs/content/docs/integrations/file.mdx:79">
P2: When a regex uses an uppercase escape or class such as `\D` or `[A-Z]`, this description says it switches to case-sensitive matching. The implementation ignores uppercase letters in escapes and character classes, so document only uppercase literal characters as affecting case sensitivity.</violation>
</file>

<file name="apps/sim/lib/workspace-files/search/text.ts">

<violation number="1" location="apps/sim/lib/workspace-files/search/text.ts:135">
P2: When a regex match itself exceeds 2 KiB, such as `abc.*` on a long line, this passes the full SQL range into the preview layout. The final byte cap then cuts the match without reserving or appending an omission marker, so the result looks complete and is not match-centred. Bound the displayed match and reserve ellipsis bytes before truncating.</violation>
</file>

<file name="apps/sim/lib/workspace-files/search/regex.ts">

<violation number="1" location="apps/sim/lib/workspace-files/search/regex.ts:70">
P2: For `\Y`, `\m`, or `\M`, the error tells callers to use a pattern with different semantics. Remove these misleading replacements or provide an actually equivalent supported alternative.</violation>

<violation number="2" location="apps/sim/lib/workspace-files/search/regex.ts:100">
P2: Patterns with a required variable repeat are rejected even when their minimum repetition creates the required literal run. Incorporate the minimum number of copies into the capped `prefix`, `suffix`, and `best` guarantee before applying the gate.</violation>
</file>

<file name="apps/sim/lib/workspace-files/search/repository.ts">

<violation number="1" location="apps/sim/lib/workspace-files/search/repository.ts:215">
P2: When a regex mixes an anchored and unanchored alternative, such as `^foo|bar`, `pattern.wholeLineOnly` is true for the entire pattern. This filter drops valid `bar` matches from lines longer than 16,384 characters; restrict the whole-line filter only when every possible alternative is anchored.</violation>
</file>

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Re-trigger cubic

Comment thread apps/docs/content/docs/integrations/file.mdx Outdated
Comment thread apps/sim/lib/workspace-files/search/text.ts
Comment thread apps/sim/lib/workspace-files/search/regex.ts
Comment thread apps/sim/lib/workspace-files/search/regex.ts
Comment thread apps/sim/lib/workspace-files/search/repository.ts
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 19 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Fix all with cubic | Re-trigger cubic

Comment thread apps/docs/content/docs/integrations/file.mdx Outdated
Comment thread apps/sim/lib/workspace-files/search/regex.ts Outdated
Comment thread apps/sim/lib/workspace-files/search/regex.ts
icecrasher321 and others added 5 commits September 1, 2026 16:03
Search read its query as literal text. It now reads it as a line-oriented
regular expression by default, with a Match setting on the block to go back
to verbatim text.

The segment store and its `gin_trgm_ops` index already support this: pg_trgm
extracts trigrams from a regex source too, so `~` / `~*` plan as a bitmap
index scan exactly like `LIKE` / `ILIKE`. No migration, no new index.

One compiled pattern owns every mode-specific decision — how PostgreSQL
matches a segment, whether the segment must hold a whole line, and where the
match sits inside it — so the repository builds one query shape and the
preview renderer one preview shape. Compilation happens in the application
use case, not the route adapter, so every surface gets the same semantics.

The supported syntax is the intersection of PostgreSQL ARE and JavaScript
RegExp, because the same source drives both the indexed predicate and the
client-side match location a preview centres on. Anything the two engines
read differently is rejected by name rather than silently reinterpreted, and
`\b` is rewritten to `\y` on the way to PostgreSQL.

Safety, in four independent layers:

- A pattern must contain 3 consecutive literal characters every match will
  include. pg_trgm indexes nothing shorter, and an unextractable pattern
  plans as a sequential scan across every workspace's segments.
- `new RegExp` proves it compiles in JavaScript.
- PostgreSQL proves it compiles in ARE; 2201B becomes a 400, not a 500.
- `statement_timeout` bounds the read. This one covers exact matching too,
  which has always been able to reach the same scan through a
  punctuation-only or non-ASCII query.

`mode` is a builder setting, withheld from the model like `maxResults`. The
model cannot see it and the two readings disagree on every metacharacter, so
`toolEnrichment` replaces the declared syntax with the active mode's — a
regex sent to a block set to exact matching would otherwise be searched for
verbatim and silently find nothing.

Verified against PostgreSQL 17: 14 behavioural checks end to end, a live
search over 150,012 segments in 14ms, 6/6 representative patterns reaching
the trigram index, and the guard cutting a 12s pattern at 10.08s into an
actionable message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preview rendering ran the user's compiled pattern with `RegExp.exec` to centre
the excerpt on the match. `RegExp` matches by backtracking, and the literal-run
gate admits nested quantifiers, so `(a+)+bcd` against a long segment cost 768ms
at 40 leading `a`s and doubles with each one — synchronously, on the event loop,
once per returned row, and entirely outside the statement timeout that bounds
the query which found the row.

PostgreSQL runs that same pattern in 0.49ms: its engine does not backtrack, and
`regexp_instr` runs inside the read's transaction, so locating a match can never
cost more than having found it. Regex mode now selects the match offsets
alongside the row and `findMatchRange` returns null for it, which is the
interface's contract rather than an omission. Exact mode is unchanged — scanning
for a known string is linear.

PostgreSQL counts characters where JavaScript slices by UTF-16 unit, so the
offsets are converted by walking the segment rather than assuming either width.

Also fixes two audit failures: `getErrorMessage` in place of a hand-written
`instanceof Error` ternary, and regenerated tool metadata and integration docs
for the search params.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… waits

Three defects from review, none of which the tests caught:

`{n,}` was rejected. `readQuantifierAt` reports an unbounded maximum as
Infinity, and the repeat cap compared it directly, so every open-ended repeat
failed as "exceeds 1000" — a form the tool's own documentation offers. Only a
stated maximum is measured now, and the minimum always is, since that is what
an expansion unrolls.

Query bounds and literal runs were measured in UTF-16 units while claiming
characters, so two astral characters read as four and slipped a gate written
for three. Both now count characters, which is also what pg_trgm indexes.

`lock_timeout` was set without classifying what it raises. A wait on
conflicting DDL surfaced as an unclassified server error, and folding it in
with the timeout arm would have told the caller to fix a pattern that is
already correct. It now maps to a distinct error the caller is told to retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… hints

Four review findings, each reproduced before it was changed.

A regex match has no length limit, so `abc.*` on a long line produces a match
larger than the whole preview budget. The layout passed it through whole and
let the final byte cap cut it, which removed the closing marker along with the
text — 2048 bytes of output ending mid-line with nothing to say so. The match
is now clipped against a budget that reserves that marker, and a clipped match
always carries one.

A variable repeat was scored at one occurrence when its minimum forces more:
`(?:ab){2,5}` cannot match without `abab` in it, but the run was counted as 2
and the pattern rejected against a gate of 3. It now contributes the copies its
minimum forces.

`\Y`, `\m` and `\M` were rejected with a suggestion to write `\b`, `^` or `$`.
Those are different assertions — a non-boundary, and two word edges rather than
the line's — so the hint handed back different semantics as a fix. They now say
no supported escape means the same thing. `\y`, `\A` and `\Z` keep theirs,
which are genuine.

Smart case was documented as reacting to any uppercase letter, but it reads
literals only, so `\D` and `[A-Z]` do not make a search case-sensitive. The
tool, block and generated docs now say what the code does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tool promised "each match" while the query is distinct on file and line,
so several matches on one line return one row. An agent reading the contract
would have expected otherwise; it now says each matching line once.

A repetition of a non-fixed atom was scored at what one copy guarantees, but
from two copies on its own tail and head meet: every match of
`(?:a(?:x|y)bc){2}` contains `bca`, which neither copy contains alone. That
run is now credited, so patterns the index can serve are no longer rejected.

Scores are capped alongside the strings they measure. Joining two capped
strings yields twice the cap, which `concatenate` could already exceed — the
gate never noticed, since it only compares against three, but the bound is
documented and now holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 19 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Fix all with cubic | Re-trigger cubic

Comment thread apps/docs/content/docs/integrations/file.mdx Outdated
Comment thread apps/docs/content/docs/integrations/file.mdx Outdated
Comment thread apps/sim/lib/workspace-files/search/regex.ts Outdated
Comment thread apps/docs/content/docs/integrations/file.mdx Outdated
An upper bound too large for `Number` arrives as Infinity, and the exception
that lets `{n,}` skip the repeat cap could not tell the two apart — so
`needle{1,<400 digits>}` passed the cap that `needle{1,5000}` fails. The
quantifier now records whether a bound was written at all, and a written one
must be at or under the cap however large it is.

The tool promised every active workspace file. It searches what the index
currently holds: a file still pending, failed, or skipped as unsupported is
not searched, and an agent reading "every file" would take an empty result as
proof of absence. Both descriptions now say so and point at `complete` and
`indexStatus`, which already carry the detail.

The declared query description spoke only for regex mode, which is what the
catalog and the generated docs render — so a builder using exact matching was
told to write a regular expression and to obey a rule that does not apply to
them. It now names both readings; the runtime schema is still enriched with
whichever is in force. The docs overview said literal text, which stopped
being true when regex became the default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 19 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/workspace-files/search/regex.ts Outdated
`[:class:]` was rejected while `[=equivalence=]` and `[.collating.]` were
forwarded unchanged. All three are PostgreSQL bracket expressions with no
JavaScript counterpart, and the parser exists to admit only what both engines
spell the same way — so two of them passed an allowlist whose whole point is
to close, and were accepted by documentation that says POSIX classes are not
supported.

They are now rejected by the construct they open, each named in its own error.
An ordinary class holding a literal dot, `[.]` or `[a.b]`, is untouched: the
form only matches on a bracket nested inside a class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 19 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant